package com.zone.mybatis.reflection;

import com.zone.mybatis.reflection.invoker.GetFieldInvoker;
import com.zone.mybatis.reflection.invoker.Invoker;
import com.zone.mybatis.reflection.invoker.MethodInvoker;
import com.zone.mybatis.reflection.invoker.SetFieldInvoker;
import com.zone.mybatis.reflection.property.PropertyNamer;

import java.lang.reflect.*;
import java.util.*;

/**
 * @author: zongzi
 * @description: TODO
 * @date: 2024/1/3
 */
public class Reflector {

    private final Class<?> type;
    private Constructor<?> defaultConstructor;
    private final Map<String, Invoker> getMethods = new HashMap<>();
    private final Map<String, Invoker> setMethods = new HashMap<>();
    private final Map<String, Class<?>> getTypes = new HashMap<>();
    private final Map<String, Class<?>> setTypes = new HashMap<>();

    private final Map<String, String> caseInsensitivePropertyMap = new HashMap<>();

    public Reflector(Class<?> clazz) {
        this.type = clazz;
        this.addDefaultConstructor(clazz);
        this.addGetMethods(clazz);
        this.addSetMethods(clazz);
        this.addFields(clazz);

        for (String propertyName : getMethods.keySet()) {
            caseInsensitivePropertyMap.put(propertyName.toUpperCase(Locale.ENGLISH), propertyName);
        }
        for (String propertyName : setMethods.keySet()) {
            caseInsensitivePropertyMap.put(propertyName.toUpperCase(Locale.ENGLISH), propertyName);
        }
    }

    public static Reflector forClass(Class<?> clazz) {
        return new Reflector(clazz);
    }

    private void addDefaultConstructor(Class<?> clazz) {
        Constructor<?>[] constructors = clazz.getDeclaredConstructors();
        for (Constructor<?> constructor : constructors) {
            if (constructor.getParameterCount() == 0) {
                if (canAccessPrivateMethods()) {
                    try {
                        constructor.setAccessible(true);
                    } catch (SecurityException e) {
                        // nothing to do
                    }
                }
                if (constructor.isAccessible()) {
                    this.defaultConstructor = constructor;
                }
            }
        }
    }

    private void addGetMethods(Class<?> clazz) {
        // 继承可能导致一个属性有多个getter方法
        Map<String, List<Method>> conflictMethods = new HashMap<>();
        Method[] methods = this.getClassMethods(clazz);
        for (Method method : methods) {
            if (method.getParameterCount() > 0) {
                continue;
            }
            String name = method.getName();
            if ((name.startsWith("get") && name.length() > 3)
                    || (name.startsWith("is") && name.length() > 2)) {
                String propertyName = PropertyNamer.methodToProperty(name);
                addConflictMethods(conflictMethods, propertyName, method);
            }
        }
        resolveConflictGetters(conflictMethods);
    }

    /**
     * 解析getters方法，保留返回值类型为继承树最底层的getter方法。
     *
     * @param conflictMethods 冲突的方法
     */
    private void resolveConflictGetters(Map<String, List<Method>> conflictMethods) {
        for (Map.Entry<String, List<Method>> entry : conflictMethods.entrySet()) {
            String propertyName = entry.getKey();
            List<Method> methods = entry.getValue();
            Method winner = null;
            for (Method method : methods) {
                if (winner == null) {
                    winner = method;
                    continue;
                }
                Class<?> winnerReturnType = winner.getReturnType();
                Class<?> candidateReturnType = method.getReturnType();
                if (winnerReturnType.equals(candidateReturnType)) {
                    // getXxx or isXxx
                    if (!boolean.class.equals(candidateReturnType)) {
                        String errorMsg = String.format("Naming convention getter fail winnerReturnType = %s; candidateReturnType = %s", winnerReturnType, candidateReturnType);
                        throw new RuntimeException(errorMsg);
                    } else if (candidateReturnType.getName().startsWith("is")) {
                        winner = method;
                    }
                } else if (candidateReturnType.isAssignableFrom(winnerReturnType)) {
                    // 候选返回值类型是Winner类的超类
                } else if (winnerReturnType.isAssignableFrom(candidateReturnType)) {
                    winner = method;
                } else {
                    // 返回值类型不相同且两个类间没有关系，不符合继承的约定，抛出异常
                    String errorMsg = String.format("Naming convention getter fail winnerReturnType = %s; candidateReturnType = %s", winnerReturnType, candidateReturnType);
                    throw new RuntimeException(errorMsg);
                }
            }
            MethodInvoker invoker = new MethodInvoker(winner);
            getMethods.put(propertyName, invoker);
            getTypes.put(propertyName, invoker.getType());
        }
    }

    private void addSetMethods(Class<?> clazz) {
        // 为什么set方法会存在冲突？泛型
        Map<String, List<Method>> conflictSetters = new HashMap<>();
        Method[] methods = this.getClassMethods(clazz);
        for (Method method : methods) {
            if (method.getName().startsWith("set")
                    && method.getName().length() > 3
                    && method.getParameterCount() == 1) {
                String property = PropertyNamer.methodToProperty(method.getName());
                addConflictMethods(conflictSetters, property, method);
            }
        }
        resolveConflictSetters(conflictSetters);
    }

    private void resolveConflictSetters(Map<String, List<Method>> conflictSetters) {
        for (Map.Entry<String, List<Method>> entry : conflictSetters.entrySet()) {
            String propertyName = entry.getKey();
            Class<?> getType = getTypes.get(propertyName);

            Method match = null;
            for (Method setter : entry.getValue()) {
                Class<?> parameterType = setter.getParameterTypes()[0];
                if (getType.equals(parameterType)) {
                    match = setter;
                    break;
                }
                if (match == null) {
                    match = setter;
                    continue;
                }
                Class<?> matchParameterType = match.getParameterTypes()[0];
                Class<?> setterParameterType = setter.getParameterTypes()[0];
                if (matchParameterType.isAssignableFrom(setterParameterType)) {
                    match = setter;
                }
            }
            if (match == null) {
                throw new RuntimeException("setter match fail propertyName: " + propertyName);
            }
            MethodInvoker invoker = new MethodInvoker(match);
            setMethods.put(propertyName, invoker);
            setTypes.put(propertyName, invoker.getType());
        }
    }

    private void addFields(Class<?> clazz) {
        Field[] fields = clazz.getFields();
        for (Field field : fields) {
            if (!canAccessPrivateMethods()) {
                continue;
            }
            try {
                field.setAccessible(true);
            } catch (SecurityException e) {
                // nothing to do
            }
            if (field.isAccessible()) {
                String fieldName = field.getName();
                if (!getMethods.containsKey(fieldName)) {
                    GetFieldInvoker invoker = new GetFieldInvoker(field);
                    getMethods.put(fieldName, invoker);
                    getTypes.put(fieldName, invoker.getType());
                }
                if (!setMethods.containsKey(fieldName)) {
                    if (!(Modifier.isFinal(field.getModifiers()) && Modifier.isStatic(field.getModifiers()))) {
                        SetFieldInvoker invoker = new SetFieldInvoker(field);
                        setMethods.put(fieldName, invoker);
                        setTypes.put(fieldName, invoker.getType());
                    }
                }
            }
        }
    }

    private boolean canAccessPrivateMethods() {
        boolean flag = true;
        try {
            SecurityManager sm = System.getSecurityManager();
            if (sm != null) sm.checkPermission(new ReflectPermission("suppressAccessChecks"));
        } catch (Exception e) {
            flag = false;
        }
        return flag;
    }

    /**
     * 获取类的方法。不调用Class.getMethods()，不仅仅是获取公共方法，还想获取private方法
     *
     * @param clazz
     * @return
     */
    private Method[] getClassMethods(Class<?> clazz) {
        Map<String, Method> uniqueMethod = new HashMap<>();
        while (clazz != null) {
            Method[] declaredMethods = clazz.getDeclaredMethods();
            this.addUniqueMethods(uniqueMethod, declaredMethods);

            // clazz有可能是抽象类，添加接口中定义的方法
            for (Class<?> interfaceClazz : clazz.getInterfaces()) {
                Method[] interfaceMethods = interfaceClazz.getMethods();
                this.addUniqueMethods(uniqueMethod, interfaceMethods);
            }
            clazz = clazz.getSuperclass();
        }

        Collection<Method> methods = uniqueMethod.values();
        return methods.toArray(new Method[methods.size()]);
    }

    private void addUniqueMethods(Map<String, Method> uniqueMethod, Method[] methods) {
        for (Method method : methods) {
            // 虚拟机生成的桥接方法不加入方法列表
            if (method.isBridge()) {
                continue;
            }
            String signature = this.getSignature(method);
            if (!uniqueMethod.containsKey(signature)) {
                if (canAccessPrivateMethods()) {
                    try {
                        method.setAccessible(true);
                    } catch (SecurityException e) {
                        // nothing to do
                    }
                }
                uniqueMethod.put(signature, method);
            }
        }
    }

    /**
     * 获取方法签名
     *
     * @param method
     * @return returnType#methodName:params1,params2
     */
    private String getSignature(Method method) {
        StringBuilder stringBuilder = new StringBuilder();
        Class<?> returnType = method.getReturnType();
        stringBuilder.append(returnType.getName())
                .append("#")
                .append(method.getName());
        Class<?>[] parameterTypes = method.getParameterTypes();
        if (parameterTypes.length > 0) {
            stringBuilder.append(":");
            for (int i = 0; i < parameterTypes.length; i++) {
                stringBuilder.append(parameterTypes[i].getName());
                // 最后一个参数不需要追加“，”
                if (i < parameterTypes.length - 1) {
                    stringBuilder.append(",");
                }
            }
        }
        return stringBuilder.toString();
    }

    private void addConflictMethods(Map<String, List<Method>> conflictMap, String name, Method method) {
        List<Method> conflictMethods = conflictMap.computeIfAbsent(name, k -> new ArrayList<>());
        conflictMethods.add(method);
    }

    public Class<?> getType() {
        return type;
    }

    public Invoker getGetInvoker(String propertyName) {
        return getMethods.get(propertyName);
    }

    public Invoker getSetInvoker(String propertyName) {
        return setMethods.get(propertyName);
    }

    public Class<?> getGetType(String propertyName) {
        Class<?> clazz = getTypes.get(propertyName);
        if (clazz == null) {
            throw new ReflectionException(String.format("Class: %s there not property name = %s not getType", type.getName(), propertyName));
        }
        return clazz;
    }

    public Class<?> getSetType(String propertyName) {
        Class<?> clazz = setTypes.get(propertyName);
        if (clazz == null) {
            throw new ReflectionException(String.format("Class: %s there not property name = %s not setType", type.getName(), propertyName));
        }
        return clazz;
    }

    public Constructor<?> getDefaultConstructor() {
        return defaultConstructor;
    }

    public boolean hasGetter(String propertyName) {
        return getMethods.containsKey(propertyName);
    }

    public boolean hasSetter(String propertyName) {
        return setMethods.containsKey(propertyName);
    }

    public String findPropertyName(String propertyName) {
        return caseInsensitivePropertyMap.get(propertyName.toUpperCase(Locale.ENGLISH));
    }
}
